/*
 * @Description: 微服务类（原创）
 * @Autor: HuiSir<www.zuifengyun.com>
 * @Date: 2022-04-22 10:30:44
 * @LastEditTime: 2022-07-07 19:24:02
 */
import "reflect-metadata"
import net from 'node:net'
import { v1 as uuidv1 } from 'uuid'
import useResult from '../hooks/useResult.js'
import { IResErrCatch } from '../hooks/useResponseError.js'
import { type IData, type IObj } from '../common/types'
import { print } from '../hooks/useLog.js'
import {
    CLASS_PREFIX_METADATA_KEY,
    CLASS_SIGN_METADATA_KEY,
    METHOD_PARAMKEYARR_METADATA_KEY
} from "../common/constants.js"
const { error } = useResult()


// 流程：
// 1、客户端给服务端发消息(携带消息id) ->
// 2、服务端接到消息进行处理 ->
// 3、服务端将处理结果反馈给客户端(携带原id) ->
// 4、客户端接到反馈，根据id找到当前消息对应的反馈信息再进行处理
// 在这个过程中，服务费发送反馈后，所有客户端都将收到所有的反馈，要根据id选出对应的反馈
// 这里排除服务端主动给客户端发消息的可能，发布订阅不存在这种方式
// 客户端有多个，服务端只有一个
// 一个服务端能接受多个客户端连接；一个客户端也能同时连接多个服务端

/**
 * server连接配置
 */
export interface IServerOpts {
    name: string // 微服务名称（唯一标识）
    port: number
    pin?: string  // 秘钥
    host?: string | '127.0.0.1'
}

/**
 * 消息内容实体
 */
export declare interface IResult {
    ok: 0 | 1 | -1 | number // 成功1，失败0，报错-1
    msg: string //提示信息
    status?: 200 | 401 | 403 | 404 | 500 | 502 | 504 | number
    data?: any
}

/**
 * 消息反馈协议
 * 服务端->客户端
 */
export interface IResultMessage {
    id: string | number // 消息id/频道号，唯一
    serverName: string // 服务端名称
    clientName: string // 客户端端名称
    res: IResult
}

/**
 * 消息请求协议
 * 客户端->服务端
 */
export interface IRequestMessage {
    id?: string | number // 消息id/频道号，唯一
    connect?: boolean // 初步连接成功
    pin?: string //秘钥
    serverName: string // 服务端名称
    clientName: string // 客户端端名称
    path: string //请求路径
    data: IData // 请求参数
}

/**
 * 服务端接收连接回调
 */
export interface IConnectCallbackArg {
    t: 1 | 2 // 1-服务端监听成功；2-接收到新的客户端连接
    address: net.AddressInfo
    name: string // 微服务名称
}

/**
 * request
 */
interface IRrequestOpt {
    /**
     * 微服务名称，从`ctx.path`中获取
     * @example`/api/myservice/a/b`中的`myservice`
     */
    service: string
    /**
     * 除前缀外路径
     * @example`/api/:微服务名称/a/b`中去除`/api/:微服务名称`后的路径`/a/b`
     */
    path: string
    /**
     * 请求参数
     */
    data: IData
}

/**
 * Controller类
 */
interface IController {
    new(): Object
}

class MicroSocket extends net.Socket {
    name: string

    // 微服务名称
    constructor(name: string, options?: net.SocketConstructorOpts) {
        super(options)
        this.name = name
    }

    writeJSON(data: IObj, callback?: (err?: Error) => void) {
        this.write(JSON.stringify(data) + 'END', callback)
    }
}

/**
 * 扩展socket 
 */
function mySocket(socket: net.Socket): MicroSocket {
    (socket as MicroSocket).writeJSON = (data: IObj, callback?: (err?: Error) => void) => {
        socket.write(JSON.stringify(data) + 'END', callback)
    };
    (socket as MicroSocket).name = '';
    return socket as MicroSocket
}


/**
 * 无效连接
 */
function errLink(socket: MicroSocket, data: Buffer | string, errorCallback: Function, error?: any) {
    const subdata = data.slice(0, 32)
    const err = error || new Error(`无效连接：${subdata.toString()} ...`)
    errorCallback && errorCallback(err)
    socket.writeJSON({ conn: 0, msg: '无效连接' })
    // 销毁
    socket.destroy()
}

/**
 * 验证Controller且归类
 */
function defineControllers(Controller: IController | IController[]) {
    const controllers = Array.isArray(Controller) ? Controller : [Controller]

    // 缓存器
    const pathControllerMethodMap: { [key: string]: Function } = {}  // 精确匹配
    const regPathControllerMethodMap: { [key: string]: Function } = {} // 正则匹配

    controllers.forEach(Contro => {
        const { name, prototype } = Contro

        // 非构造函数报错
        if (!(name && prototype)) {
            throw new Error(`Uncaught TypeError: The parameter \`${Contro}\` of the method \`Micro.controllers()\` is not a constructor!`)
        }

        // 非Controller类报错
        const classSign = Reflect.getMetadata(CLASS_SIGN_METADATA_KEY, prototype, name)
        if (!classSign || classSign !== 'Controller') {
            throw new Error(`Class \`${name}\` is not a controller class, please use the class decorator \`@Controller()\`!`)
        }

        // 检测是否有相同的前缀
        const prefix = Reflect.getMetadata(CLASS_PREFIX_METADATA_KEY, prototype, name)
        Object.getOwnPropertyNames(prototype).forEach((methodKey) => {
            if (methodKey !== 'constructor') {

                const path = prefix ? `/${prefix}/${methodKey}` : `/${methodKey}`

                const paramKeys: string[] = Reflect.getMetadata(METHOD_PARAMKEYARR_METADATA_KEY, prototype, methodKey)

                if (paramKeys) {
                    const regPath = `^${path}${paramKeys.map(key => `/(?<${key}>[A-Za-z0-9_-]+)`).join("")}$`
                    // 检测是否有相同方法
                    if (regPathControllerMethodMap[regPath]) {
                        throw new Error(`Method \`${methodKey}\` already have the same name in the other controller!`)
                    }
                    // 这里必须使用bind绑定this指向实例对象，否则在方法中找不到this
                    regPathControllerMethodMap[regPath] = prototype[methodKey].bind(prototype)
                } else {
                    if (pathControllerMethodMap[path]) {
                        throw new Error(`Method \`${methodKey}\` already have the same name in the other controller!`)
                    }
                    pathControllerMethodMap[path] = prototype[methodKey].bind(prototype)
                }

            }
        })
    })

    return {
        pathMap: pathControllerMethodMap,
        regMap: regPathControllerMethodMap
    }
}


/**
 * 微服务消息推送系统
 * use tcp
 */
export default class Micro {
    // tcp服务器
    private server: net.Server

    // 服务器socket连接对象(数组)
    private serverSockets: MicroSocket[] = []

    // tcp客户端socket连接对象(数组)
    // 一个客户端可以连接多个服务器
    private clientSockets: MicroSocket[] = []

    // 微服务名称
    readonly name: string

    // 报错回调
    private errorCallback: (err: Error) => void

    // 服务器关闭回调
    private serverCloseCallbacks: Array<() => void> = []

    // 服务端接消息
    private serverMessageCallback: (message: IRequestMessage) => void

    // 客户端接消息
    private clientMessageCallbacks: Array<(message: IResultMessage) => void> = []

    /**
     * 构造函数
     * @param name 微服务名称
     */
    constructor(name: string) {
        this.name = name
    }

    /**
     * 中间件调用
     */
    use(middleware: (options?: any) => void, options?: any): Micro {
        middleware.call(this, options)
        return this
    }

    /**
     * 监听
     * @param server 当前服务器配置
     * @param callback 回调
     */
    listen(server: IServerOpts, callback?: (arg: IConnectCallbackArg) => void): Micro {
        if (!(server.host && server.name && server.pin && server.port)) {
            if (this.errorCallback) {
                this.errorCallback(new Error('server参数有误！name、pin、host、port四项缺一不可！'))
            }
            return
        }

        if (server.name.length > 36 || server.pin.length > 36) {
            if (this.errorCallback) {
                this.errorCallback(new Error('name与pin长度不能超过36位！'))
            }
            return
        }

        // 创建服务器实例
        this.server = net.createServer((socket) => {

            const _socket = mySocket(socket)
            let buffer = Buffer.from('')
            let dataEnd = false
            let timer: any = null

            /**
             * 监听消息
             */
            _socket.on("data", async (data) => {
                buffer = Buffer.concat([buffer, data])

                // 如果不是json直接报错
                if (!['[', '{'].includes(buffer.slice(0, 1).toString())) {
                    errLink(_socket, buffer, this.errorCallback)
                    return
                }

                // 超时检测(这里不传文件，只传json，所以时间不会太久,这里初步设为1s)
                timer = timer || setTimeout(() => {
                    if (!dataEnd) {
                        errLink(_socket, buffer, this.errorCallback)
                    }
                    clearTimeout(timer)
                    timer = null
                }, 1000)

                dataEnd = data.slice(data.length - 3).toString() === 'END'

                // 传递结束
                if (!dataEnd) {
                    return
                }

                // 状态重置
                clearTimeout(timer)
                timer = null
                dataEnd = false

                // 数据解析
                try {
                    // 转换
                    const jsonData: IRequestMessage = JSON.parse(buffer.slice(0, buffer.length - 3).toString())

                    // 置空
                    buffer = Buffer.from('')

                    // 没有客户端字段
                    if (!(jsonData && jsonData.clientName && jsonData.serverName && jsonData.serverName === server.name)) {
                        errLink(_socket, jsonData.toString(), this.errorCallback)
                        return
                    }

                    // 第一次连接
                    if (jsonData.connect) {
                        // 验证秘钥，秘钥错误断开连接
                        if (jsonData.pin !== server.pin) {
                            _socket.writeJSON({ conn: 0, msg: `秘钥pin有误，无法连接到 ${server.name} 服务。` })
                            _socket.destroy()
                        } else {
                            _socket.writeJSON({ conn: 1 })
                            _socket.name = jsonData.clientName
                            // 存档
                            this.serverSockets.push(_socket)
                            // 有新的客户端接入
                            callback && callback({ t: 2, name: jsonData.clientName, address: _socket.address() as net.AddressInfo })
                        }
                        return
                    }

                    // 发送
                    this.serverMessageCallback && this.serverMessageCallback(jsonData)

                } catch (_err) {
                    // 报错
                    errLink(_socket, data, this.errorCallback)
                    return
                }
            })


            /**
             * 报错触发
             */
            _socket.on('error', (err: any) => {
                if (this.errorCallback) {
                    if (err.errno === -4077 || err.code === 'ECONNRESET' || err.message.includes('ECONNRESET')) {
                        err.message = `客户端 ${_socket.name} 非法中断，请尝试重启`
                        this.errorCallback(err)
                    } else {
                        err.message = `客户端 ${_socket.name} 连接出错，请尝试重启：` + err.message
                        this.errorCallback(err)
                    }
                }
                // 出库
                this.serverSockets.splice(this.serverSockets.findIndex(({ name }) => name == _socket.name), 1)
                // 销毁
                _socket.destroy()
            })
        })

        //设置关闭时的回调函数
        this.server.on('close', () => {
            // 清空连接
            this.serverSockets = []
            // 回调
            if (this.serverCloseCallbacks.length) {
                this.serverCloseCallbacks.forEach(cb => cb())
            }
        })

        //出错直接抛出，中断服务
        this.server.on('error', (err) => { throw err })

        // 服务器入网
        this.server.listen(server, () => {
            //获取地址信息
            callback && callback({ t: 1, name: server.name, address: this.server.address() as net.AddressInfo })
        })

        return this
    }

    /**
     * 客户端连接
     * @param server 要连接的服务器参数
     * @param callback 连接结果回调
     */
    client(server: IServerOpts, callback?: (err: null | Error, socket: MicroSocket) => void): Micro {
        if (!(server.host && server.name && server.pin && server.port)) {
            if (this.errorCallback) {
                this.errorCallback(new Error('server参数有误！name、pin、host、port四项缺一不可！'))
            }
            return
        }

        if (server.name.length > 36) {
            if (this.errorCallback) {
                this.errorCallback(new Error('server参数name有误！'))
            }
            return
        }

        if (server.pin.length > 36) {
            if (this.errorCallback) {
                this.errorCallback(new Error('server参数pin有误！'))
            }
            return
        }

        //创建TCP客户端
        const socket = new MicroSocket(server.name)

        /* 
            方法：
            socket.pause() // 暂停
            socket.emit('') // 触发事件
            socket.end(callback?: () => void) // 关闭
        */

        //设置连接的服务器
        socket.connect(server, () => {
            // 连接成功后告诉服务端此微服务的名称和秘钥
            socket.writeJSON({
                connect: true,
                serverName: socket.name,
                clientName: this.name,
                pin: server.pin
            })
        })

        // 错误事件
        socket.on("error", (err: any) => {
            if (err.message.includes('connect') || err.syscall === 'connect' || err.errno === -4078) {
                // 回调
                let newerr = new Error('connect ECONNREFUSED')
                callback && callback(newerr, socket)
                this.errorCallback && this.errorCallback(newerr)
            } else {
                err.message = '服务连接出错' + err.message
                this.errorCallback && this.errorCallback(err)
            }
        })

        // 断开、关闭
        socket.on('close', () => {
            // 清除缓存连接
            if (this.clientSockets.length) {
                const index = this.clientSockets.findIndex(({ name }) => name == socket.name)
                if (index >= 0) {
                    this.clientSockets.splice(index, 1)
                    // 销毁socket
                    socket.destroy()
                }
            }
        })

        let buffer = Buffer.from('')

        //监听服务端消息
        socket.on("data", (data) => {
            buffer = Buffer.concat([buffer, data])

            // 传递结束
            if (data.slice(data.length - 3).toString() !== 'END') {
                return
            }

            const dataObj = JSON.parse(buffer.slice(0, buffer.length - 3).toString())

            // 置空
            buffer = Buffer.from('')

            if (dataObj.conn === 0) {
                const err = new Error(dataObj.msg)
                // 回调
                callback && callback(err, socket)
                this.errorCallback && this.errorCallback(err)
                return
            }

            if (dataObj.conn === 1) {
                // 存档
                this.clientSockets.push(socket)
                // 回调
                callback && callback(null, socket)
                return
            }

            // 发送
            this.clientMessageCallbacks.length
                && this.clientMessageCallbacks.forEach(cb => cb(dataObj))
        })

        return this
    }

    /**
     * 客户端判断连接是否存在，若已断开或未连接则不存在
     * @param serverName 连接的服务端名称
     */
    hasSocket(serverName: string): boolean {
        return this.clientSockets.findIndex(({ name }) => name == serverName) >= 0
    }

    /**
     * 服务端给客户端发消息（给所有客户端）
     * @param message 要发送的数据
     * @param callback 回调
     */
    private sendMessageToClient(message: IResultMessage, callback?: (err?: Error) => void): Micro {
        if (this.serverSockets.length) {
            // 给对应的客户端发消息
            const currClientSocket = this.serverSockets.find(socket => socket.name === message.clientName)
            if (currClientSocket) {
                // 由于不确定数据会不会分片（一般每片为56KB），所以这里给个后缀`END`
                currClientSocket.writeJSON(message, (err) => {
                    if (err) {
                        callback && callback(err)
                    } else {
                        // 数据长度
                        callback && callback()
                    }
                })
            } else {
                const newerr = new Error(`客户端 ${message.clientName} 未连接`)
                callback && callback(newerr)
                this.errorCallback && this.errorCallback(newerr)
            }
        } else {
            const newerr = new Error(`客户端 ${message.clientName} 未连接`)
            callback && callback(newerr)
            this.errorCallback && this.errorCallback(newerr)
        }
        return this
    }

    /**
     * 客户端给服务端发消息
     * 不会主动发消息，所以设为私有
     * @param message 要发送的数据
     * @param callback 回调
     */
    private sendMessageToServer(message: IRequestMessage, callback?: (err?: Error) => void): Micro {
        if (this.clientSockets.length) {
            const currSocket = this.clientSockets.find(socket => socket.name === message.serverName)
            if (currSocket) {
                // 由于不确定数据会不会分片（一般每片为56KB），所以这里给个后缀`END`
                currSocket.writeJSON(message, (err) => {
                    if (err) {
                        callback && callback(err)
                    } else {
                        callback && callback()
                    }
                })
            } else {
                const newerr = new Error(`找不到 ${message.serverName} 服务，请检查路径或联系管理员！`)
                callback && callback(newerr)
                this.errorCallback && this.errorCallback(newerr)
            }
        } else {
            const newerr = new Error(`找不到 ${message.serverName} 服务，请检查路径或联系管理员！`)
            callback && callback(newerr)
            this.errorCallback && this.errorCallback(newerr)
        }
        return this
    }

    /**
     * 服务端接消息
     */
    private onServerMessage(callback: (message: IRequestMessage) => void): Micro {
        this.serverMessageCallback = callback
        return this
    }

    /**
     * 客户端接消息
     */
    private onClientMessage(callback: (message: IResultMessage) => void): Micro {
        this.clientMessageCallbacks.push(callback)
        return this
    }

    /**
     * 客户端->服务端 发请求+接反馈
     * @param requestOptions {IRrequestOpt}
     * -----------------
     * - path 除前缀之后的路径（前缀如:`/api/:微服务名称`）
     * - data 请求参数
     */
    request({ service, path, data }: IRrequestOpt): Promise<IResult> {
        // 超时：默认10秒
        const timeout = data.header?.timeout || 10000
        const id = uuidv1()
        return new Promise((resolve, reject) => {
            const timer = setTimeout(() => {
                reject(new Error('请求超时'))
                clearTimeout(timer)
            }, Number(timeout))

            this
                // 发送
                .sendMessageToServer(
                    {
                        id,
                        serverName: service,
                        clientName: this.name,
                        path,
                        data
                    },
                    (err) => {
                        clearTimeout(timer)
                        err && reject(err)
                    })
                // 接收
                .onClientMessage((message) => {
                    if (message.id === id) {
                        clearTimeout(timer)
                        resolve(message.res)
                    }
                })
        })
    }

    /**
     * 控制api分发
     * @param Controller 方法对象
     */
    controllers(Controller: IController | IController[]) {
        // 解析Controller
        const { pathMap, regMap } = defineControllers(Controller)

        this.onServerMessage(async ({ id, clientName, path, data }) => {
            let res
            const method_path = pathMap[path]
            const method_reg = regMap[Object.keys(regMap).find(regstr => {
                const match = path.match(new RegExp(regstr))
                // params赋值
                if (match) {
                    data.params = match.groups
                }
                return match
            })]

            const methodfn = method_path || method_reg

            if (!methodfn || typeof methodfn !== 'function') {
                res = error({ status: 404, msg: 'Not Found' })
            } else {
                try {
                    res = await methodfn(data)
                } catch (err: any) {
                    const { statusCode, message }: IResErrCatch = err
                    res = error({ status: statusCode || 500, msg: message || err.toString() })
                    statusCode || print.error(err)
                }
            }

            // 反馈
            this.sendMessageToClient({
                id,
                clientName,
                serverName: this.name,
                res
            })
        })
        return this
    }

    /**
     * err事件
     * @param callback error统一处理，所有报错均执行此callback
     */
    onError(callback: (error?: Error) => void): Micro {
        this.errorCallback = callback
        return this
    }

    /**
     * 服务close事件
     * @param callback 服务关闭时要执行的脚本
     */
    onServerClose(callback: () => void): Micro {
        this.serverCloseCallbacks.push(callback)
        return this
    }
}